package protocol.arq.gobackN;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.swing.Timer;

import protocol.arq.ARQ;
import protocol.arq.ARQSender;
import protocol.hdlc.HDLCProtocol;
import protocol.hdlc.TrameBuilder;
import utils.advancedSocket.AdvancedSocket;
import utils.trame.Trame;
import utils.trame.Trame.TrameType;

/**
 * Implemente le protocole d'envoi "Go-Back-N".
 * 
 * @see GoBackNReceiver
 * @author Bertholet Damien & Daoust Julien
 */
public class GoBackNSender extends ARQSender {

	/**
	 * Correspond au {@link Trame} envoye dans la fenetre courante.
	 */
	private List<Trame> sentTrame = new ArrayList<Trame>();

	/**
	 * Indique le dernier indice recu par le recepteur.
	 */
	private int lastNumReceived = -1;

	/**
	 * Les timers qui sont actuellement actif.
	 */
	private Map<Trame, Timer> timers = new HashMap<Trame, Timer>();

	/**
	 * Constructeur.
	 * 
	 * @param advancedSocket
	 *            L'{@link AdvancedSocket}.
	 * @param trameBuilder
	 *            La {@link TrameBuilder} pour avoir les prochaines donnees.
	 * @throws IOException
	 *             Si la {@link Socket} n'a pu etre cree.
	 * @throws UnknownHostException
	 *             Si la machine n'existe pas.
	 */
	public GoBackNSender(AdvancedSocket advancedSocket,
	        TrameBuilder trameBuilder) throws UnknownHostException, IOException {
		super(advancedSocket, trameBuilder);
	}

	/*
	 * (non-Javadoc)
	 * @see protocol.arq.ARQ#receiveTrame(java.lang.String)
	 */
	@Override
	protected void receivedTrame(String data) {
		// C'est qu'on a finit les envoies et qu'ils ont ete recu. On ignore
		// donc les trames recues jusqu'a ce qu'on soit deconnecte.
		if(sentTrame == null)
			return;

		Trame trame = ARQ.decodeTrame(data);

		if(HDLCProtocol.VERBOSE) {
			System.out.println("Receive :" + data);
			System.out.println(trame.toExpliciteString() + "\n");
		}

		int num = trame.getNum();
		int numReceived = getIndexWindow(num - 1);

		switch(trame.getType()) {
		case A:
			// Si on recoit un acquittement pour la connexion, on peut commencer
			// a envoyer les trames.
			if(lastNumReceived == -1 && sentTrame.isEmpty()) {
				sendNextTrame();
				return;
			}

			lastNumReceived = numReceived;
			cleanReceivedTrame();

			sendNextTrame();
			break;
		case R:
			// Si on est dans le cas initial et qu'on a un rej sur la premiere
			// Trame.
			if(lastNumReceived == -1 && numReceived == 6) {
				transferTramesForResending();
				sendNextTrame();
				return;
			}

			if(lastNumReceived != numReceived) {
				lastNumReceived = numReceived;
				cleanReceivedTrame();
			}

			// Tout le reste est a renvoyer.
			transferTramesForResending();
			sendNextTrame();
			break;
		case C:
		case F:
		case I:
		case P:
		default:
			// On ne fait rien.
			break;
		}
	}

	/**
	 * Nettoie les {@link Trame} deja envoye qui ont recu un acquittement.
	 */
	private void cleanReceivedTrame() {
		boolean find = false;

		while(!find && !sentTrame.isEmpty()) {
			Trame trameTemp = sentTrame.get(0);
			sentTrame.remove(trameTemp);

			// On enleve le timer actif.
			Timer timer = timers.get(trameTemp);
			if(timer != null) {
				timers.remove(trameTemp);
				timer.stop();
			}

			if(trameTemp.getNum() == lastNumReceived)
				find = true;
		}

		// Si l'accuse de reception concerne une Trame que l'on a decide de
		// renvoyer une autre fois a cause d'un probleme.
		if(!find) {
			Trame trameTemp;
			do
				trameTemp = trameBuilder.builtNextTrame();
			while(trameTemp != null && trameTemp.getNum() != lastNumReceived);
		}
	}

	/**
	 * Envoie la prochaine {@link Trame}, si possible.
	 */
	private void sendNextTrame() {
		try {
			Trame trame = trameBuilder.builtNextTrame();

			// Il n'y a plus de donnees a envoyer.
			if(trame == null) {
				// Et on a recu toutes les confirmations de receptions.
				if(sentTrame.isEmpty())
					endConnection();

				return;
			}

			sendTrame(trame);
		} catch (RuntimeException e) {
			// Erreur de lecture des donnees pour la construction de la trame.
			closeConnectionError(e.getMessage());
			return;
		}

		if(sentTrame.size() < windowSize)
			sendNextTrame();
	}

	@Override
	protected void sendTrame(Trame trame) {
		super.sendTrame(trame);

		sentTrame.add(trame);

		Timer timer = new Timer(HDLCProtocol.TIMER_TIME, null);
		ActionListener listener = builtActionListenerTimer(timer, trame);
		timer.addActionListener(listener);

		timer.start();
		timers.put(trame, timer);
	}

	/**
	 * Construit un {@link ActionListener} pour le {@link Timer}. Il gere le
	 * temporisateur.
	 * 
	 * @param timer
	 *            Le {@link Timer}.
	 * @param trame
	 *            La {@link Trame} qui a ete envoye.
	 * @return L'{@link ActionListener}.
	 */
	private ActionListener builtActionListenerTimer(final Timer timer,
	        final Trame trame) {
		return new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				stopAllTimers();

				// Pour ne pas etre deranger par un autre timer.
				if(sentTrame.isEmpty() || sentTrame.get(0) != trame)
					return;

				sendPBit();
				transferTramesForResending();
				sendNextTrame();
			}
		};
	}

	/**
	 * Met toutes les {@link Trame} qui ont ete envoyes mais qui ne sont pas
	 * acquite (celles qui sont dans 'sentTrame') dans les {@link Trame} a
	 * 'renvoyer'.
	 */
	private void transferTramesForResending() {
		while(!sentTrame.isEmpty()) {
			Trame trame = sentTrame.get(sentTrame.size() - 1);
			sentTrame.remove(trame);

			// On enleve le timer actif.
			Timer timer = timers.get(trame);
			if(timer != null) {
				timers.remove(trame);
				timer.stop();
			}

			trameBuilder.pushTrame(trame);
		}
	}

	/**
	 * Envoie un message P-Bit.
	 */
	private void sendPBit() {
		super.sendTrame(new Trame(TrameType.P, 0));
	}

	/**
	 * Arrete tous les timers.
	 */
	private void stopAllTimers() {
		for(Timer timer: timers.values())
			timer.stop();
	}

	/*
	 * (non-Javadoc)
	 * @see protocol.arq.ARQSender#startConnection()
	 */
	@Override
	public void startConnection() {
		final Trame connectionTrame = new Trame(TrameType.C, getMethod());
		super.sendTrame(connectionTrame);

		final Timer timer = new Timer(HDLCProtocol.TIMER_TIME, null);
		ActionListener listener = new ActionListener() {

			/**
			 * Nombre de fois maximun que l'on renvoie une {@link Trame} afin
			 * d'abandonner la connexion.
			 */
			private final int NB_MAX_SENDING = 3;

			private int nb_sending = 1;

			@Override
			public void actionPerformed(ActionEvent e) {
				if(lastNumReceived != -1 || !sentTrame.isEmpty()) {
					timer.stop();
					return;
				}

				if(nb_sending == NB_MAX_SENDING) {
					timer.stop();
					closeConnectionError("Sending limit has been reached.");
					return;
				}

				nb_sending++;
				GoBackNSender.super.sendTrame(connectionTrame);
			}
		};
		timer.addActionListener(listener);

		timer.start();
	}

	/**
	 * Envoie la trame de fin de connexion.
	 */
	private void endConnection() {
		sentTrame = null;
		super.sendTrame(new Trame(TrameType.F, 0));
	}

	/*
	 * (non-Javadoc)
	 * @see protocol.arq.ARQ#method()
	 */
	@Override
	protected int getMethod() {
		return 0;
	}
}